package cn.lijiajia3515.cairo.auth.framework.security.oauth2.client.userinfo;

import cn.hutool.core.bean.BeanUtil;
import cn.lijiajia3515.cairo.auth.domain.mongo.AccountMongo;
import cn.lijiajia3515.cairo.auth.domain.mongo.AuthMongoConstants;
import cn.lijiajia3515.cairo.auth.domain.mongo.ConnectionMongo;
import cn.lijiajia3515.cairo.auth.framework.AccountRepository;
import cn.lijiajia3515.cairo.auth.framework.security.oauth2.CairoOAuth2User;
import cn.lijiajia3515.cairo.auth.modules.auth.AuthType;
import cn.lijiajia3515.cairo.core.CoreConstants;
import cn.lijiajia3515.cairo.domain.CairoAccount;
import cn.lijiajia3515.cairo.mongo.MongoConstants;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.http.RequestEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CairoOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
	private final String CLIENT = "cairo";
	private final MongoTemplate mongoTemplate;
	private final AccountRepository repository;
	private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";

	private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";

	private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new CommonOAuth2UserRequestEntityConverter();
	private final OAuthUserInfoResponseClient<OAuth2Userinfo> userInfoResponseClient = new CommonOAuth2UserInfoResponseClient();


	public CairoOAuth2UserService(MongoTemplate mongoTemplate, AccountRepository repository) {
		this.mongoTemplate = mongoTemplate;
		this.repository = repository;
	}

	@Override
	public CairoOAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
		Assert.notNull(userRequest, "userRequest cannot be null");
		if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
			OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
				"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
					+ userRequest.getClientRegistration().getRegistrationId(),
				null);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
		String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
		if (!StringUtils.hasText(userNameAttributeName)) {
			OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE, "Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: " + userRequest.getClientRegistration().getRegistrationId(), null);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
		RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
		OAuth2Userinfo response = userInfoResponseClient.getResponse(userRequest, request);

		return Optional.ofNullable(response)
			.flatMap(userinfo -> {
				Set<GrantedAuthority> authorities = new LinkedHashSet<>();
				OAuth2AccessToken token = userRequest.getAccessToken();
				for (String authority : token.getScopes()) {
					authorities.add(new SimpleGrantedAuthority("scp_" + authority));
				}

				String connection = OAuth2ProviderConnection.connection(userRequest.getClientRegistration().getRegistrationId());
				String subject = userinfo.subject();

				Criteria connectionCriteria = Criteria
					.where(ConnectionMongo.FIELD.META.DELETED).is(MongoConstants.NO_DELETED_VALUE)
					.and(ConnectionMongo.FIELD.CONNECTION).is(connection)
					.and(ConnectionMongo.FIELD.SUBJECT).is(subject);
				Query connectionQuery = Query.query(connectionCriteria)
					.with(Sort.by(
						Sort.Order.asc(ConnectionMongo.FIELD.META.CREATED.AT),
						Sort.Order.asc(ConnectionMongo.FIELD.META.LAST_MODIFIED.AT),
						Sort.Order.asc(ConnectionMongo.FIELD._ID)
					));
				connectionQuery.fields().include(ConnectionMongo.FIELD.ACCOUNT);
				ConnectionMongo cm = mongoTemplate.findOne(connectionQuery, ConnectionMongo.class, AuthMongoConstants.Collection.CONNECTION);

				return Optional.ofNullable(cm)
					.map(ConnectionMongo::getAccount)
					// 如果账号为空 则 创建新账号
					.or(() -> createAccount(connection, userinfo).map(AccountMongo::getId))
					.map(x -> createAuthenticationToken(x, userinfo, authorities));
			}).orElseThrow(() -> new OAuth2AuthenticationException("未知异常"));
	}

	/**
	 * 创建 认证凭证
	 *
	 * @param accountId   账号id
	 * @param userinfo    oauth2 用户信息
	 * @param authorities oauth2 权限
	 * @return 凭证
	 */
	public CairoOAuth2User createAuthenticationToken(String accountId, OAuth2Userinfo userinfo, Collection<GrantedAuthority> authorities) {
		CairoAccount principal = repository.getAccount(CLIENT, accountId);
		List<? extends GrantedAuthority> newAuthorities = Stream.of(repository.getAuthority(CLIENT, accountId), authorities).flatMap(Collection::stream).collect(Collectors.toList());
		CairoOAuth2User user = CairoOAuth2User.builder()
			.id(principal.getId())
			.login(principal.getLogin())
			.email(principal.getEmail())
			.phoneNumber(principal.getPhoneNumber())
			.nickname(principal.getNickname())
			.avatarUrl(principal.getAvatarUrl())
			.oAuth2Userinfo(userinfo)
			.attributes(BeanUtil.beanToMap(userinfo))
			.authorities(newAuthorities)
			.build();

		AuthType type;
		if (userinfo instanceof WechatWeb2Userinfo) {
			type = AuthType.Wechat;
		} else if (userinfo instanceof Github2Userinfo) {
			type = AuthType.Github;
		} else {
			type = AuthType.OAuth2;
		}

		user.setType(type);
		return user;
	}

	@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
	public Optional<AccountMongo> createAccount(String connection, OAuth2Userinfo userinfo) {
		AccountMongo account;
		if (userinfo instanceof WechatWeb2Userinfo) {
			account = createOAuthAccount(connection, (WechatWeb2Userinfo) userinfo);
		} else if (userinfo instanceof Github2Userinfo) {
			account = createOAuthAccount(connection, (Github2Userinfo) userinfo);
		} else {
			account = createOAuthAccount(connection, userinfo.subject());
		}
		return Optional.of(account);
	}

	@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
	public AccountMongo createOAuthAccount(String connection, String subject) {
		String uid = CoreConstants.SNOWFLAKE.nextIdStr();
		ConnectionMongo cm = ConnectionMongo.builder()
			.connection(connection)
			.subject(subject)
			.account(uid)
			.info(Collections.emptyMap())
			.build();

		AccountMongo account = AccountMongo.builder()
			.id(uid)
			.username(uid)
			.role(Collections.emptyMap())
			.department(Collections.emptyMap())
			.authority(Collections.emptyMap())
			.resource(Collections.emptyMap())
			.build();

		cm = mongoTemplate.insert(ConnectionMongo.class)
			.inCollection(AuthMongoConstants.Collection.CONNECTION)
			.one(cm);

		account = mongoTemplate.insert(AccountMongo.class)
			.inCollection(AuthMongoConstants.Collection.ACCOUNT)
			.one(account);

		return account;
	}

	@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
	public AccountMongo createOAuthAccount(String connection, WechatWeb2Userinfo userinfo) {
		String accountId = CoreConstants.SNOWFLAKE.nextIdStr();

		ConnectionMongo cm = ConnectionMongo.builder()
			.connection(connection)
			.subject(userinfo.subject())
			.account(accountId)
			.info(userinfo)
			.build();

		AccountMongo account = AccountMongo.builder()
			.id(accountId)
			.username(accountId)
			.name(userinfo.getNickname())
			.avatarUrl(userinfo.getHeadimgurl())
			.accountAdmin(Collections.emptyMap())
			.role(Collections.emptyMap())
			.department(Collections.emptyMap())
			.resource(Collections.emptyMap())
			.authority(Collections.emptyMap())
			.build();

		cm = mongoTemplate.insert(ConnectionMongo.class)
			.inCollection(AuthMongoConstants.Collection.CONNECTION)
			.one(cm);

		account = mongoTemplate.insert(AccountMongo.class)
			.inCollection(AuthMongoConstants.Collection.ACCOUNT)
			.one(account);

		return account;
	}

	@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
	public AccountMongo createOAuthAccount(String connection, Github2Userinfo userinfo) {
		String accountId = CoreConstants.SNOWFLAKE.nextIdStr();

		ConnectionMongo cm = ConnectionMongo.builder()
			.connection(connection)
			.subject(userinfo.subject())
			.account(accountId)
			.info(userinfo)
			.build();

		AccountMongo account = AccountMongo.builder()
			.id(accountId)
			.username(accountId)
			.name(userinfo.getName())
			.avatarUrl(userinfo.getAvatarUrl())
			.accountAdmin(Collections.emptyMap())
			.role(Collections.emptyMap())
			.department(Collections.emptyMap())
			.authority(Collections.emptyMap())
			.resource(Collections.emptyMap())
			.build();

		cm = mongoTemplate.insert(ConnectionMongo.class)
			.inCollection(AuthMongoConstants.Collection.CONNECTION)
			.one(cm);

		account = mongoTemplate.insert(AccountMongo.class)
			.inCollection(AuthMongoConstants.Collection.ACCOUNT)
			.one(account);

		return account;
	}

	/**
	 * Sets the {@link Converter} used for converting the {@link OAuth2UserRequest} to a
	 * {@link RequestEntity} representation of the UserInfo Request.
	 *
	 * @param requestEntityConverter the {@link Converter} used for converting to a
	 *                               {@link RequestEntity} representation of the UserInfo Request
	 * @since 5.1
	 */
	public final void setRequestEntityConverter(Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter) {
		Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
		this.requestEntityConverter = requestEntityConverter;
	}


}
